/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package simhya.model.store.jepstore;

import simhya.model.store.*;
import java.util.*;
import org.nfunk.jep.*;
import org.nfunk.jep.function.PostfixMathCommand;
import simhya.utils.RandomGenerator;






/**
 *
 * @author Luca
 */
public class JepStore implements Store {
    private JEP evaluator;
    private ArrayList<Variable> variablesInit;
    private ArrayList<Variable> parametersInit;
    private ArrayList<Node> predicatesInit;
    private ArrayList<Node> functionsInit;

    private ArrayList<Predicate> externalPredicates;
    private ArrayList<Function> externalFunctions;

    private Variable[] variables;
    private Variable[] parameters;
    private Node[] predicates;
    private Node[] functions;

    private HashMap<String,Integer> variableLookupTable;
    private HashMap<String,Integer> parameterLookupTable;
    private HashSet<FunctionSignature> functionsSignature;

    private boolean initialized;
    private boolean variablesInitialized;

    public JepStore() {
        variablesInitialized = false;
        initialized = false;
        this.evaluator = new JEP();
        this.evaluator.setImplicitMul(true);
        this.evaluator.addStandardFunctions();
        this.evaluator.addStandardConstants();
        this.variables = null;
        this.parameters = null;
        this.functions = null;
        this.predicates = null;
        this.variablesInit = new ArrayList();
        this.parametersInit = new ArrayList();
        this.functionsInit = new ArrayList();
        this.predicatesInit = new ArrayList();
        this.externalFunctions = new ArrayList();
        this.externalPredicates = new ArrayList();
        this.variableLookupTable = new HashMap();
        this.parameterLookupTable = new HashMap();
        this.functionsSignature = new HashSet();
        PostfixMathCommand f = new Uniform2();
        evaluator.getFunctionTable().put("uniform", f);
        f = new Uniform();
        evaluator.getFunctionTable().put("uniform", f);
        f = new Min();
        evaluator.getFunctionTable().put("min", f);
        f = new Max();
        evaluator.getFunctionTable().put("max", f);

    }


    public void finalizeVariableInitialization() {
        this.variables = this.variablesInit.toArray(new Variable[0]);
        this.parameters = this.parametersInit.toArray(new Variable[0]);
        this.variablesInit = null;
        this.parametersInit = null;
        variablesInitialized = true;
    }

    public void finalizeInitialization() {
        
        this.functions = this.functionsInit.toArray(new Node[0]);
        this.predicates = this.predicatesInit.toArray(new Node[0]);
        this.predicatesInit = null;
        this.functionsInit = null;
        for (Function f : this.externalFunctions)
            f.initialize();
        for (Predicate p : this.externalPredicates)
            p.initialize();
        this.initialized = true;
    }

    public void addExternalFunction(Function f) {
         if (initialized)
            throw new StoreException("Store already initialized");
        this.externalFunctions.add(f);
    }

    public void addExternalPredicate(Predicate p) {
         if (initialized)
            throw new StoreException("Store already initialized");
        this.externalPredicates.add(p);
    }





    /**
     *
     * @return the number of variables in the store
     */
    public int getNumberOfVariables() {
        return this.variables.length;
    }

    /**
     *
     * @return the number of parameters in the store
     */
    public int getNumberOfParameters() {
        return this.parameters.length;
    }

    /**
     * Evaluates the predicate identified by predicateID
     * @param predicateID the ID of the predicate
     * @return the value of the predicate
     */
    public boolean evaluatePredicate(int predicateID){
            // if the store initializatio is not completed, the following vector is null;
            //if (!this.initialized)
            //    throw new StoreException("Store initialization not completed");
            //not needed, an out of bounds exception will be thrown by the following instrunction
            //if (predicateID >= this.predicates.length)
            //    throw new StoreException("Predicate does not exists " + predicateID);
            
            try {  
                if ( ((Double)this.evaluator.evaluate(predicates[predicateID])).doubleValue() > 0.0 )
                    return true;
                else
                    return false;
            }
            catch(ParseException e) {
                throw new StoreException("Evaluate predicate exception: " + e);
            }
    }

    /**
     * Computes the value of the function identified by functionID
     * @param functionID the ID of the function
     * @return the value of the function
     */
    public double computeFunction(int functionID) {
       // if the store initializatio is not completed, the following vector is null;
       //if (!this.initialized)
       //    throw new StoreException("Store initialization not completed");
//        if (functionID >= this.functions.length)
//                throw new StoreException("Function does not exists " + functionID);
        
        try {
             return ((Double)this.evaluator.evaluate(functions[functionID])).doubleValue();
        }
        catch(ParseException e) {
             throw new StoreException("Evaluate function exception: " + e);
        }
    }

    /**
     * Adds a predicate to the store.
     * @param predicate a string containing the predicate
     * @return a Predicate object
     */
    public  Predicate addPredicate(String predicate) {
         if (initialized)
            throw new StoreException("Store already initialized");
        try {
            Node n = this.evaluator.parse(predicate);
            int i = this.predicatesInit.size();
            this.predicatesInit.add(n);
            Predicate p = new GenericPredicate(i,this);
            return p;
        }
        catch(ParseException e) {
             throw new StoreException("Parsing predicate " + predicate + " failed: " + e);
        }
    }

    /**
     * Adds a new function to the store.
     * @param function a string containing a function
     * @return a Function object
     */
    public  Function addFunction(String function) {
         if (initialized)
            throw new StoreException("Store already initialized");
        try {
            Node n = this.evaluator.parse(function);
            int i = this.functionsInit.size();
            this.functionsInit.add(n);
            Function f = new GenericFunction(i,this);
            return f;
        }
        catch(ParseException e) {
             throw new StoreException("Parsing function " + function + " failed: " + e);
        }
    }
   

    /**
     * Adds a variable to the store, with given name and initial value.
     * @param name a string with a given name. A
     * @param value the initial value of the variable
     * @return the id code of the new added variable.
     */
    public int addVariable(String name, double value) {
        if (initialized)
            throw new StoreException("Store already initialized");
        if (variablesInitialized)
            throw new StoreException("Variables already initialized");
        if (this.variableLookupTable.containsKey(name) || this.parameterLookupTable.containsKey(name))
            throw new StoreException("Variable " + name + " already defined");
        evaluator.addVariable(name, value);
        Variable v = evaluator.getVar(name);
        int i = this.variablesInit.size();
        this.variablesInit.add(v);
        this.variableLookupTable.put(name,i);
        return i;
    }

    /**
     * Adds a new parameter to the store, with given name and value
     * @param name the name of the parameter
     * @param value the value of the parameter
     * @return the id code of the parameter
     */
    public int addParameter(String name, double value) {
         if (initialized)
            throw new StoreException("Store already initialized");
        if (variablesInitialized)
            throw new StoreException("Variables already initialized");
        if (this.variableLookupTable.containsKey(name) || this.parameterLookupTable.containsKey(name))
            throw new StoreException("Parameter " + name + " already defined");
        evaluator.addConstant(name, value);
        Variable v = evaluator.getVar(name);
        int i = this.parametersInit.size();
        this.parametersInit.add(v);
        this.parameterLookupTable.put(name,i);
        return i;
    }

    /**
     * Returns the ID of a variable.
     * Throws an exception if the variable is not in the store.
     * @param name the name of te variable
     * @return the id of the variable
     */
    public int getVariableID(String name) {
        if (!this.variableLookupTable.containsKey(name))
            throw new StoreException("Variable " + name + " is not in the store");
        return this.variableLookupTable.get(name);
    }

    /**
     * Returns the ID of a parameter.
     * Throws an exception if the parameter is not in the store.
     * @param name the name of te variable
     * @return the id of the variable
     */
    public int getParameterID(String name) {
        if (!this.parameterLookupTable.containsKey(name))
            throw new StoreException("Parameter " + name + " is not in the store");
        return this.parameterLookupTable.get(name);

    }


    /**
     * Returns the value of a variable
     * @param variableID the id of the variable
     * @return the current value of the variable in the store
     */
    public double getVariableValue(int variableID) {
        // if the store initializatio is not completed, the following vector is null;
       //if (!this.initialized)
       //    throw new StoreException("Store initialization not completed");
        //if (variableID >= this.variables.size())
        //    throw new StoreException("Variable with ID " + variableID + " does not exist");
        return (Double)variables[variableID].getValue();
    }

     /**
     * Returns the value of a parameter
     * @param parameterID the id of the parameter
     * @return the current value of the parameter in the store
     */
    public double getParameterValue(int parameterID) {
        // if the store initializatio is not completed, the following vector is null;
       //if (!this.initialized)
       //    throw new StoreException("Store initialization not completed");
        //if (parameterID >= this.parameters.size())
        //    throw new StoreException("Parameter with ID " + parameterID + " does not exist");
        return (Double)parameters[parameterID].getValue();
    }


    /**
     * Sets the current value of variable with id variableID
     * @param variableID the ID of the variable
     * @param value the new value
     */
    public void setVariableValue(int variableID, double value) {
        // if the store initializatio is not completed, the following vector is null;
       //if (!this.initialized)
       //    throw new StoreException("Store initialization not completed");
        //if (variableID >= this.variables.size())
        //    throw new StoreException("Variable with ID " + variableID + " does not exist");
        variables[variableID].setValue(value);
    }


    /**
     * Allows to change the value of a parameter
     * @param parameterID the ID of the parameter
     * @param value the new value
     */
    public void setParameterValue(int parameterID, double value) {
        // if the store initializatio is not completed, the following vector is null;
       //if (!this.initialized)
       //    throw new StoreException("Store initialization not completed");
         //if (parameterID >= this.parameters.size())
         //   throw new StoreException("Parameter with ID " + parameterID + " does not exist");
        parameters[parameterID].setIsConstant(false);
        parameters[parameterID].setValue(value);
        parameters[parameterID].setIsConstant(true);
    }


    /**
     *
     * @return a list with the current value of all variables,
     * according to their id
     */
    public ArrayList<Double> getVariablesValueAsList()  {
        // if the store initializatio is not completed, the following vector is null;
       //if (!this.initialized)
       //    throw new StoreException("Store initialization not completed");
        ArrayList<Double> list = new ArrayList();
        for (Variable v : this.variables)
            list.add((Double)v.getValue());
        return list;
    }

    /**
     *
     * @return a list with the value of all parameters,
     * according to their id
     */
    public ArrayList<Double> getParametersValueAsList() {
        // if the store initializatio is not completed, the following vector is null;
       //if (!this.initialized)
       //    throw new StoreException("Store initialization not completed");
        ArrayList<Double> list = new ArrayList();
        for (Variable v : this.parameters)
            list.add((Double)v.getValue());
        return list;
    }

    public Object[] getParameters() {
        // if the store initializatio is not completed, the following vector is null;
       //if (!this.initialized)
       //    throw new StoreException("Store initialization not completed");
        return this.parameters;
    }

    public Object[] getVariables() {
        // if the store initializatio is not completed, the following vector is null;
       //if (!this.initialized)
       //    throw new StoreException("Store initialization not completed");
        return this.variables;
    }

    public double[] getVariablesValues() {
       throw new StoreException("Unsupported operation. Variables are encapsulated as objects. "
               + "Use getVariables() instead");
    }

    public double[] getParametersValues() {
       throw new StoreException("Unsupported operation. Parameters are encapsulated as objects. "
               + "Use getParameters() instead");
    }

    public double[] getCopyOfVariablesValues() {
       // if the store initializatio is not completed, the following vector is null;
       //if (!this.initialized)
       //    throw new StoreException("Store initialization not completed");
        double [] values = new double[variables.length];
        for (int i=0;i<variables.length;i++)
            values[i] = (Double)variables[i].getValue();
        return values;
    }

    public double[] getCopyOfParametersValues() {
        // if the store initializatio is not completed, the following vector is null;
       //if (!this.initialized)
       //    throw new StoreException("Store initialization not completed");
        double [] values = new double[parameters.length];
        for (int i=0;i< parameters.length;i++)
            values[i] = (Double)parameters[i].getValue();
        return values;
    }






    /**
     * Sets the value of all variables as specified
     * @param values a list of new values for all variables
     */
    public void setAllVariableValues(ArrayList<Double> values) {
        // if the store initializatio is not completed, the following vector is null;
       //if (!this.initialized)
       //    throw new StoreException("Store initialization not completed");
        if (values.size() != this.variables.length)
            throw new StoreException("Size mismatch between list of values (" +values.size() + ") and number of variables (" + this.variables.length +")");
        for (int i=0;i<values.size();i++)
            variables[i].setValue(values.get(i));
    }

     /**
     * Sets the value of all parameters as specified
     * @param values a list of new values for all variables
     */
    public void setAllParameterValues(ArrayList<Double> values) {
        // if the store initializatio is not completed, the following vector is null;
       //if (!this.initialized)
       //    throw new StoreException("Store initialization not completed");
        if (values.size() != this.parameters.length)
            throw new StoreException("Size mismatch between list of values (" +values.size() + ") and number of parameters (" + this.variables.length +")");
        for (int i=0;i<values.size();i++)
             parameters[i].setValue(values.get(i));
    }

    public void setAllParameterValues(double[] values) {
        // if the store initializatio is not completed, the following vector is null;
       //if (!this.initialized)
       //    throw new StoreException("Store initialization not completed");
        if (values.length != this.variables.length)
            throw new StoreException("Size mismatch between list of values (" +values.length + ") and number of variables (" + this.variables.length +")");
        for (int i=0;i<values.length;i++)
            variables[i].setValue(values[i]);
    }

    public void setAllVariableValues(double[] values) {
        // if the store initializatio is not completed, the following vector is null;
       //if (!this.initialized)
       //    throw new StoreException("Store initialization not completed");
        if (values.length != this.parameters.length)
            throw new StoreException("Size mismatch between list of values (" +values.length + ") and number of parameters (" + this.variables.length +")");
        for (int i=0;i<values.length;i++)
             parameters[i].setValue(values[i]);
    }

    public Object getParametersReference() {
        throw new UnsupportedOperationException("Not supported by this store.");
    }

    public Object getVariablesReference() {
        throw new UnsupportedOperationException("Not supported by this store.");
    }

    public void setAllParameterValues(Object values) {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    public void setAllVariableValues(Object values) {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    public void setParameterValuesReference(Object values) {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    public void setVariableValuesReference(Object values) {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    public void newEvaluationRound() {
        throw new UnsupportedOperationException("Not supported yet.");
    }









    public String getVariablesRepresentation() {
        String s = "";
        ArrayList<String> names = this.getNameOfAllVariables();
        for (int i=0;i<this.variables.length;i++)
            s += (i > 0 ? "; " : "") + String.format(Locale.US, "%s=%.5f", names.get(i), variables[i]);
        return s;
    }


    

    //public double getVariableValue(String variableName) { }
    //public double getParameterValue(String parameterName) { }
    //public void setVariableValue(String variableName, double value) {  }
    //public void setParameterValue(String parameterName, double value) {  }
    //public  Predicate addPredicate(Node predicate) { throw new RuntimeException("To be implemented"); }
    //public  Function addFunction(Node function) { throw new RuntimeException("To be implemented"); }

    /**
     * collects the ID of variables appearing in a predicate and returns them n a list
     * @param id the id of the predicate
     * @return a list of integers
     */
    public ArrayList<Integer> getVariableListOfPredicate(int id) {
        // if the store initializatio is not completed, the following vector is null;
       //if (!this.initialized)
       //    throw new StoreException("Store initialization not completed");
        if (id >= this.predicates.length)
                throw new StoreException("Predicate does not exists " + id);
        Node n = predicates[id];
        ArrayList<Integer> list = new ArrayList();
        this.getVariableList(n,list);
        return list;
    }

     /**
     * collects the ID of variables appearing in a function and returns them in a list
     * @param id the id of the function
     * @return a list of integers
     */
    public ArrayList<Integer> getVariableListOfFunction(int id) {
        // if the store initializatio is not completed, the following vector is null;
       //if (!this.initialized)
       //    throw new StoreException("Store initialization not completed");
        if (id >= this.functions.length)
           throw new StoreException("Function does not exists " + id);
        Node n = functions[id];
        ArrayList<Integer> list = new ArrayList();
        this.getVariableList(n,list);
        return list;
    }

    /**
     * Recursively visits the expression tree of a jep node and collects the ID of
     * variables appearing in it.
     * @param n the node
     * @param list the list to be filled
     */
    private void  getVariableList(Node n, ArrayList<Integer> list) {
        try {
            String name = ((ASTVarNode) n).getName();
            if (!((ASTVarNode) n).getVar().isConstant())
                list.add(this.variableLookupTable.get(name));
        }
        catch(ClassCastException e) { }
        for (int i=0;i<n.jjtGetNumChildren();i++)
            getVariableList(n.jjtGetChild(i),list);
    }


    /**
     * Adds a definition of a new function to the store, allowing to call it in other expressions.
     * Notice that recursive definition of functions is NOT allowed!
     * @param funcName the name of the function
     * @param funcParams a list of formal parameters that are to be passed to the function
     * @param funcDef a string defining the function.
     */
    public void addNewFunctionDefinition(String funcName,ArrayList<String> funcParams, String funcDef) {
        int k = funcParams.size();
        FunctionSignature sig = new FunctionSignature(k,funcName);
        if (this.functionsSignature.contains(sig))
            throw new StoreException("Function with name " + funcName + " and arity " + k + " has already been defined ");
        GenericJepFunction f = new GenericJepFunction(funcName);
        for (String p : funcParams)
            f.addFunctionParameter(p);
        f.addFunctionDefinition(funcDef);
        evaluator.getFunctionTable().put(funcName, f);
    }

    /**
     * 
     * @return a list with the name of all variables
     */
    public ArrayList<String> getNameOfAllVariables() {
        // if the store initializatio is not completed, the following vector is null;
       //if (!this.initialized)
       //    throw new StoreException("Store initialization not completed");
        ArrayList<String> list = new ArrayList();
        for (Variable v : this.variables)
            list.add(v.getName());
        return list;
    }

    /**
     *
     * @return a list with the name of all parameters
     */
    public ArrayList<String> getNameOfAllParameters() {
        // if the store initializatio is not completed, the following vector is null;
       //if (!this.initialized)
       //    throw new StoreException("Store initialization not completed");
        ArrayList<String> list = new ArrayList();
        for (Variable v : this.parameters)
            list.add(v.getName());
        return list;
    }
    
    public class Uniform2 extends PostfixMathCommand {

        public Uniform2() {
            numberOfParameters = 2;
        }

        @Override
        public void run(Stack inStack) throws ParseException {
            //assigns the value to the formal parameters and computes the value of the expression!
            checkStack(inStack);// check the stack

            Double param2 = (Double)inStack.pop();
            Double param1 = (Double)inStack.pop();
            double r = RandomGenerator.nextDouble();
            Double x = param1 + (param2 - param1)*r;

            inStack.push(x);//push the result on the inStack
            return;
        }
    }

    public class Max extends PostfixMathCommand {

        public Max() {
            numberOfParameters = -1;
        }

        @Override
        public void run(Stack inStack) throws ParseException {
            //assigns the value to the formal parameters and computes the value of the expression!
            checkStack(inStack);// check the stack
            if (curNumberOfParameters < 1) throw new ParseException("No arguments for Max");

            Double m = (Double)inStack.pop();
            Double p;
	    int i = 1;

            // repeat summation for each one of the current parameters
            while (i < curNumberOfParameters) {
                    p = (Double)inStack.pop();
                    m = Math.max(p, m);
                    i++;
            }
            inStack.push(m);
            return;
        }
    }

    public class Min extends PostfixMathCommand {

        public Min() {
            numberOfParameters = -1;
        }

        @Override
        public void run(Stack inStack) throws ParseException {
            //assigns the value to the formal parameters and computes the value of the expression!
            checkStack(inStack);// check the stack
            if (curNumberOfParameters < 1) throw new ParseException("No arguments for Min");
                
            Double m = (Double)inStack.pop();
            Double p;
	    int i = 1;

            // repeat summation for each one of the current parameters
            while (i < curNumberOfParameters) {
                    p = (Double)inStack.pop();
                    m = Math.min(p, m);
                    i++;
            }
            inStack.push(m);
            return;
        }
    }

     public class Uniform extends PostfixMathCommand {

        public Uniform() {
            numberOfParameters = 0;
        }

        @Override
        public void run(Stack inStack) throws ParseException {
            //assigns the value to the formal parameters and computes the value of the expression!
            checkStack(inStack);// check the stack
            Double x = RandomGenerator.nextDouble();
            inStack.push(x);//push the result on the inStack
            return;
        }
    }

    public class GenericJepFunction extends PostfixMathCommand {
    private String name;
    private HashMap<String,String> parameterMap;
    private ArrayList<String> parameters;
    private Node function;
    

    public GenericJepFunction(String name) {
        this.name = name;
        numberOfParameters = 0;
        this.parameterMap = new HashMap();
        this.parameters = new ArrayList();
    }



    public void addFunctionParameter(String par) {
        numberOfParameters++;
        String p = "__" + name + "_" + par;
        this.parameterMap.put(par, p);
        this.parameters.add(p);
        //adds the hidden variable to the store!
        evaluator.addVariable(p, 0);
    }

    public void addFunctionDefinition(String func)  {
        try {
            JEP j = new JEP();
            j.setImplicitMul(true);
            j.setAllowUndeclared(true);
            //abilitas undefined variables?
            Node n = j.parse(func);
            String f = this.replaceParams(n);
            this.function = evaluator.parse(f);
        }
        catch (ParseException e) {
            throw new StoreException("Exception while parsing function definition: " + e);
        }
       
    }

    /**
     * recursively replaces parameters with new local name
     * @param n a node of an expression tree
     */
    private String replaceParams(Node n) {
        String s = "";
        try {
            if (!((ASTFunNode) n).isOperator()) {
                String s1 = ((ASTFunNode) n).getName();
                s += s1 + "(";
                for (int i=0;i<n.jjtGetNumChildren();i++)
                    s += (i>0 ? "," : "") + replaceParams(n.jjtGetChild(i));
                s += ")";
            }
            else {
                String s1 = ((ASTFunNode) n).getName();
                int k = ((ASTFunNode) n).jjtGetNumChildren();
                if (k == 2)
                    s += replaceParams(n.jjtGetChild(0)) + " " + s1 + " " + replaceParams(n.jjtGetChild(1));
                else if (k==1)
                    s += " " + s1 + replaceParams(n.jjtGetChild(0));
                else
                    throw new StoreException("Found Operator " + s1 + " with a number of parameters different from 1 or 2!!!");
            }
        }
        catch(ClassCastException e) { }
        try {
            s = ((ASTVarNode) n).getName();
            if (this.parameterMap.containsKey(s))
                s = this.parameterMap.get(s);
        }
        catch(ClassCastException e) { }
        
        return s;
    }


   


    @Override
    public void run(Stack inStack) throws ParseException {
        //assigns the value to the formal parameters and computes the value of the expression!
        for (int i=numberOfParameters-1;i>=0;i--) {
            checkStack(inStack);// check the stack
            Object param = inStack.pop();
            evaluator.setVarValue(this.parameters.get(i), param);
        }
        try {
            Object x = evaluator.evaluate(this.function);
            inStack.push(x);//push the result on the inStack
            return;
        }
        catch (ParseException e) {
            throw new StoreException("Exception while evaluating custom function: " + e);
        }
    }


}

    public class FunctionSignature {
        public int arity;
        public String name;

        public FunctionSignature(int arity, String name) {
            this.arity = arity;
            this.name = name;
        }

        @Override
        public boolean equals(Object obj) {
            if (!obj.getClass().getName().equals(this.getClass().getName()))
                return false;
            FunctionSignature x = (FunctionSignature)obj;
            return (x.arity == this.arity) && (x.name.equals(this.name));
        }


    }

}

